﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using PSE.Framework.Threading;
using PSE.Framework.ErrorLogging;
using System.Threading;

namespace PSE.Framework.IO
{
    public class FileSystemWatcher : IDisposable
    {
        private List<FileEntryInfo> _files = new List<FileEntryInfo>();
        private SyncTimer _timer;
        private ExecutionQueue _executionQueue;
        private System.IO.FileSystemWatcher _fileSystemWatcher;
        private bool _hasChanges = false;

        private object _lockHasChanges = new object();
        private object _lockFiles = new object();

        private string _path = null;

        public string Path
        {
            get { return _path; }
            set
            {
                this.CheckIsEnabled();

                if (string.IsNullOrEmpty(value))
                    throw new ArgumentNullException("Path");

                if (_fileSystemWatcher != null)
                    _fileSystemWatcher.Path = value;

                _path = value;
            }
        }

        private string _filter = "*";

        public string Filter
        {
            get { return _filter; }
            set
            {
                this.CheckIsEnabled();

                if (string.IsNullOrEmpty(value))
                    throw new ArgumentNullException("Filter");

                if (_fileSystemWatcher != null)
                    _fileSystemWatcher.Filter = value;

                _filter = value;
            }
        }

        private bool _includeSubdirectories = false;

        public bool IncludeSubdirectories
        {
            get { return _includeSubdirectories; }
            set
            {
                this.CheckIsEnabled();

                if (_fileSystemWatcher != null)
                    _fileSystemWatcher.IncludeSubdirectories = value;

                _includeSubdirectories = value;
            }
        }

        private TimeSpan _interval = TimeSpan.FromSeconds(1);

        public TimeSpan Interval
        {
            get { return _interval; }
            set
            {
                this.CheckIsEnabled();

                if (_timer != null)
                    _timer.Interval = value;

                _interval = value;
            }
        }

        private bool _enabled = false;

        public bool Enabled
        {
            get { return _enabled; }
        }

        private void CheckIsEnabled()
        {
            if (this.Enabled)
                throw new InvalidOperationException("Cannot change file watcher attributes while it's enabled.");
        }

        public FileSystemWatcher()
            : this(null)
        {
        }

        public FileSystemWatcher(string path)
            : this(path, "*")
        {
        }

        public FileSystemWatcher(string path, string filter)
            : this(path, filter, TimeSpan.FromSeconds(1))
        {
        }

        public FileSystemWatcher(string path, string filter, TimeSpan interval)
        {
            _timer = new SyncTimer();
            _timer.Elapsed += new EventHandler(ElapsedHandler);

            _executionQueue = new ExecutionQueue();

            _fileSystemWatcher = new System.IO.FileSystemWatcher();

            _fileSystemWatcher.Created += new FileSystemEventHandler(CreatedHandler);
            _fileSystemWatcher.Changed += new FileSystemEventHandler(ChangedHandler);
            _fileSystemWatcher.Deleted += new FileSystemEventHandler(DeletedHandler);
            _fileSystemWatcher.Renamed += new RenamedEventHandler(RenamedHandler);
            _fileSystemWatcher.Error += new ErrorEventHandler(ErrorHandler);

            _fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
            _fileSystemWatcher.InternalBufferSize = 64 * 4096;
            _fileSystemWatcher.EnableRaisingEvents = false;

            if (!string.IsNullOrEmpty(path))
                this.Path = path;
            this.Filter = filter;
            this.Interval = interval;
        }

        public void Start()
        {
            if (!this.Enabled)
            {
                _enabled = true;

                _files = this.GetFiles();

                _timer.Start();
                _fileSystemWatcher.EnableRaisingEvents = true;
            }
        }

        public void Stop()
        {
            if (this.Enabled)
            {
                _timer.Stop();
                _fileSystemWatcher.EnableRaisingEvents = false;

                _files.Clear();

                _enabled = false;
            }
        }

        private List<FileEntryInfo> GetFiles()
        {
            DirectoryInfo rootDir = new DirectoryInfo(this.Path);

            IEnumerable<System.IO.FileSystemInfo> directories = rootDir.GetDirectories(this.Filter, this.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Cast<FileSystemInfo>();
            IEnumerable<System.IO.FileSystemInfo> files = rootDir.GetFiles(this.Filter, this.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Cast<FileSystemInfo>();

            IEnumerable<System.IO.FileSystemInfo> entries = directories.Concat(files);

            return entries.OrderBy(f => f.FullName).Select(f => new FileEntryInfo(f.FullName, f.LastWriteTime, f is System.IO.FileInfo)).ToList();
        }

        private void ElapsedHandler(object sender, EventArgs e)
        {
            bool hasChanges;

            lock (_lockHasChanges)
            {
                hasChanges = _hasChanges;
                _hasChanges = false;
            }

            if (hasChanges)
            {
                lock (_lockFiles)
                {
                    List<FileEntryInfo> oldFiles = _files;

                    foreach (FileEntryInfo oldFile in oldFiles)
                    {
                        // Se o arquivo antigo não existir mais, dispara Deleted
                        if (!oldFile.Exists)
                        {
                            this.OnDeleted(new FileSystemEventArgs(WatcherChangeTypes.Deleted, oldFile.DirectoryName, oldFile.Name));
                        }
                        else
                        {
                            // Se o nome do arquivo antigo tiver mudado, dispara Renamed
                            if (oldFile.IsRenamed)
                            {
                                this.OnRenamed(new RenamedEventArgs(WatcherChangeTypes.Renamed, oldFile.DirectoryName, oldFile.Name, oldFile.OldName));
                            }

                            // Se o arquivo antigo estiver diferente do novo, dispara Changed
                            if (oldFile.HasChanges)
                            {
                                this.OnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, oldFile.DirectoryName, oldFile.Name));
                            }
                        }
                    }

                    List<FileEntryInfo> newfiles = this.GetFiles();

                    foreach (FileEntryInfo newFile in newfiles)
                    {
                        // Se o arquivo novo não existir na lista antiga, dispara Created
                        if (!oldFiles.Any(f => f.FullName == newFile.FullName))
                        {
                            this.OnCreated(new FileSystemEventArgs(WatcherChangeTypes.Created, newFile.DirectoryName, newFile.Name));
                        }
                    }

                    _files = newfiles;
                }
            }
        }

        private void ChangedHandler(object sender, System.IO.FileSystemEventArgs e)
        {
            lock (_lockHasChanges)
            {
                _hasChanges = true;
            }
        }

        private void CreatedHandler(object sender, System.IO.FileSystemEventArgs e)
        {
            lock (_lockHasChanges)
            {
                _hasChanges = true;
            }
        }

        private void DeletedHandler(object sender, System.IO.FileSystemEventArgs e)
        {
            lock (_lockHasChanges)
            {
                _hasChanges = true;
            }
        }

        private void RenamedHandler(object sender, System.IO.RenamedEventArgs e)
        {
            lock (_lockHasChanges)
            {
                _hasChanges = true;
            }

            lock (_lockFiles)
            {
                FileEntryInfo renamedFile = _files.FirstOrDefault(f => f.FullName == e.OldFullPath);

                if (renamedFile != null)
                    renamedFile.Rename(e.FullPath);
            }
        }

        private void ErrorHandler(object sender, ErrorEventArgs e)
        {
            lock (_lockHasChanges)
            {
                _hasChanges = true;
            }

            ErrorLogger.WriteLog(LogType.Warning, e.GetException());
        }

        public event System.IO.FileSystemEventHandler Created;

        protected virtual void OnCreated(System.IO.FileSystemEventArgs e)
        {
            if (Created != null)
                _executionQueue.QueueUserWorkItem(new WaitCallback(s => Created(this, e)));
        }

        public event System.IO.FileSystemEventHandler Changed;

        protected virtual void OnChanged(System.IO.FileSystemEventArgs e)
        {
            if (Changed != null)
                _executionQueue.QueueUserWorkItem(new WaitCallback(s => Changed(this, e)));
        }

        public event System.IO.FileSystemEventHandler Deleted;

        protected virtual void OnDeleted(System.IO.FileSystemEventArgs e)
        {
            if (Deleted != null)
                _executionQueue.QueueUserWorkItem(new WaitCallback(s => Deleted(this, e)));
        }

        public event System.IO.RenamedEventHandler Renamed;

        protected virtual void OnRenamed(System.IO.RenamedEventArgs e)
        {
            if (Renamed != null)
                _executionQueue.QueueUserWorkItem(new WaitCallback(s => Renamed(this, e)));
        }

        public void Dispose()
        {
            this.Stop();

            _hasChanges = false;

            if (_timer != null)
            {
                _timer.Dispose();
                _timer.Elapsed -= new EventHandler(ElapsedHandler);
                _timer = null;
            }

            if (_fileSystemWatcher != null)
            {
                _fileSystemWatcher.Dispose();

                _fileSystemWatcher.Created -= new FileSystemEventHandler(CreatedHandler);
                _fileSystemWatcher.Changed -= new FileSystemEventHandler(ChangedHandler);
                _fileSystemWatcher.Deleted -= new FileSystemEventHandler(DeletedHandler);
                _fileSystemWatcher.Renamed -= new RenamedEventHandler(RenamedHandler);
                _fileSystemWatcher.Error -= new ErrorEventHandler(ErrorHandler);

                _fileSystemWatcher = null;
            }
        }

        private class FileEntryInfo
        {
            public FileEntryInfo(string fullName, DateTime lastWriteTime, bool isFile)
            {
                this.FullName = fullName;
                this.LastWriteTime = lastWriteTime;
                this.IsFile = isFile;
            }

            public bool IsFile { get; private set; }


            public string FullName { get; private set; }

            public string Name
            {
                get { return System.IO.Path.GetFileName(this.FullName); }
            }

            public string DirectoryName
            {
                get { return System.IO.Path.GetDirectoryName(this.FullName); }
            }


            public string OldFullName { get; private set; }

            public string OldName
            {
                get { return System.IO.Path.GetFileName(this.OldFullName); }
            }

            public string OldDirectoryName
            {
                get { return System.IO.Path.GetDirectoryName(this.OldFullName); }
            }


            public DateTime LastWriteTime { get; private set; }


            public bool Exists
            {
                get 
                {
                    return this.IsFile ? File.Exists(this.FullName) : 
                                         Directory.Exists(this.FullName);
                }
            }

            public bool HasChanges
            {
                get
                {
                    return this.IsFile ? File.GetLastWriteTime(this.FullName) != this.LastWriteTime :
                                         Directory.GetLastWriteTime(this.FullName) != this.LastWriteTime;
                }
            }

            public bool IsRenamed
            {
                get
                {
                    return !string.IsNullOrEmpty(this.OldFullName);
                }
            }

            public void Rename(string newFullName)
            {
                if (string.IsNullOrEmpty(this.OldFullName))
                    this.OldFullName = this.FullName;

                this.FullName = newFullName;
            }
        }
    }
}
